﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Text.RegularExpressions;
using BeanboxSoftware.BeanProxy.Internals;

namespace BeanboxSoftware.BeanProxy
{
  public static partial class ProxyManager
  {
    /// <summary>
    /// Creates a proxy object of type T
    /// </summary>
    /// <typeparam name="T">Type to proxy</typeparam>
    /// <param name="ctorArgValues">arguments passed to best fit ctor of T</param>
    /// <returns>proxy</returns>
    public static T CreateProxy<T>(params object[] ctorArgValues)
    {
      var config = new CtorConfig();

      for (int i = 0; i < ctorArgValues.Length; i++)
      {
        var next = ctorArgValues[i];

        if (next == null)
        {
          throw new NullReferenceException("ProxyManager cannot build a constructor definition with a null parameter. Use " + typeof(CtorArg).Name + "s for null values");
        }

        config.Add(next.GetType(), next);
      }

      return CreateProxy<T>(config);
    }

    /// <summary>
    /// Creates a proxy object of type T
    /// </summary>
    /// <typeparam name="T">Type to proxy</typeparam>
    /// <param name="config">detailed settings for constructing a proxy</param>
    /// <returns>proxy</returns>
    public static T CreateProxy<T>(CtorConfig config)
    {
      var builder = new Builder<T>(config);
      var proxy = builder.CreateProxy();

      return proxy;
    }

    public static MethodConfig<T> GetMethod<T>(T fp)
    {
      var del = (MulticastDelegate)((object)fp);
      var iprox = GetIProxy(del.Target);

      var types = del.Method.GetGenericArguments();
      MethodDefinition signature = null;
      MethodConfig mc = null;

      if (types.Length == 0)
      {
        signature = iprox.MethodDefinitions.Values.Find(del.Method);
        mc = signature != null ? signature.MethodConfig : null;
      }
      else
      {
        var generic = del.Method.GetGenericMethodDefinition(); //e.g. void Foo<T>(T t)
        var md = iprox.MethodDefinitions.Values.Find(generic);
        var specific = md == null ? null : md.DefinedAs(del.Method.GetGenericArguments());

        if (specific != null)
        {
          if (specific.Unbound)
          {
            specific.BindTo(iprox);
          }

          mc = specific.MethodConfig;
        }
      }

      if (mc == null)
      {
        ErrorMethodNotFound(iprox, del.Method, del.Method.Name);
      }

      return new MethodConfig<T>(mc);
    }

    public static MethodConfig<T> LookupMethod<T>(object proxy, string name, params Type[] types)
    {
      var iprox = GetIProxy(proxy);
      var signature = new MethodDefinition(typeof(T), name);
      var fit = iprox.MethodDefinitions.Values.BestFit(signature, types);
      var mc = fit == null ? null : fit.MethodConfig;

      if (mc == null)
      {
        ErrorMethodNotFound(iprox, signature.MethodInfo, name);
      }

      return new MethodConfig<T>(mc);
    }

    /// <summary>
    /// Returns a method config for an Action
    /// </summary>
    /// <param name="action">function pointer</param>
    /// <returns>method configuration</returns>
    public static ActionConfig GetAction(Action action)
    {
      return new ActionConfig(GetMethod(action).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1&gt;
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <param name="action">function pointer</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1> GetAction<T1>(Action<T1> action)
    {
      return new ActionConfig<T1>(GetMethod(action).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2&gt;
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <param name="action">function pointer</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2> GetAction<T1, T2>(Action<T1, T2> action)
    {
      return new ActionConfig<T1, T2>(GetMethod(action).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2, T3&gt;
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <param name="action">function pointer</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2, T3> GetAction<T1, T2, T3>(Action<T1, T2, T3> action)
    {
      return new ActionConfig<T1, T2, T3>(GetMethod(action).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2, T3, T4&gt;
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <param name="action">function pointer</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2, T3, T4> GetAction<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action)
    {
      return new ActionConfig<T1, T2, T3, T4>(GetMethod(action).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2, T3, T4, T5&gt;
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <typeparam name="T5">param 5 type</typeparam>
    /// <param name="action">function pointer</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2, T3, T4, T5> GetAction<T1, T2, T3, T4, T5>(Action<T1, T2, T3, T4, T5> action)
    {
      return new ActionConfig<T1, T2, T3, T4, T5>(GetMethod(action).Access);
    }

    /// <summary>
    /// Returns a method config for an Action named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static ActionConfig LookupAction(object proxy, string methodName, params Type[] types)
    {
      return new ActionConfig(LookupMethod<Action>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1> LookupAction<T1>(object proxy, string methodName, params Type[] types)
    {
      return new ActionConfig<T1>(LookupMethod<Action<T1>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2> LookupAction<T1, T2>(object proxy, string methodName, params Type[] types)
    {
      return new ActionConfig<T1, T2>(LookupMethod<Action<T1, T2>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2, T3&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2, T3> LookupAction<T1, T2, T3>(object proxy, string methodName, params Type[] types)
    {
      return new ActionConfig<T1, T2, T3>(LookupMethod<Action<T1, T2, T3>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2, T3, T4&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2, T3, T4> LookupAction<T1, T2, T3, T4>(object proxy, string methodName, params Type[] types)
    {
      return new ActionConfig<T1, T2, T3, T4>(LookupMethod<Action<T1, T2, T3, T4>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for an Action&lt;T1, T2, T3, T4, T5&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <typeparam name="T5">param 5 type</typeparam>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1, T2, T3, T4, T5> LookupAction<T1, T2, T3, T4, T5>(object proxy, string methodName, params Type[] types)
    {
      return new ActionConfig<T1, T2, T3, T4, T5>(LookupMethod<Action<T1, T2, T3, T4, T5>>(proxy, methodName, types).Access);
    }

    /// <summary>
    /// Returns a method config for a Func&lt;TResult&gt;
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="func">function pointer</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<TResult> GetFunc<TResult>(Func<TResult> func)
    {
      return new FuncConfig<TResult>(GetMethod(func).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1&gt;
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <param name="func">function pointer</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, TResult> GetFunc<T1, TResult>(Func<T1, TResult> func)
    {
      return new FuncConfig<T1, TResult>(GetMethod(func).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2&gt;
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <param name="func">function pointer</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, TResult> GetFunc<T1, T2, TResult>(Func<T1, T2, TResult> func)
    {
      return new FuncConfig<T1, T2, TResult>(GetMethod(func).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2, T3&gt;
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <param name="func">function pointer</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, T3, TResult> GetFunc<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> func)
    {
      return new FuncConfig<T1, T2, T3, TResult>(GetMethod(func).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2, T3, T4&gt;
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <param name="func">function pointer</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, T3, T4, TResult> GetFunc<T1, T2, T3, T4, TResult>(Func<T1, T2, T3, T4, TResult> func)
    {
      return new FuncConfig<T1, T2, T3, T4, TResult>(GetMethod(func).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2, T3, T4, T5&gt;
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <typeparam name="T5">param 5 type</typeparam>
    /// <param name="func">function pointer</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, T3, T4, T5, TResult> GetFunc<T1, T2, T3, T4, T5, TResult>(Func<T1, T2, T3, T4, T5, TResult> func)
    {
      return new FuncConfig<T1, T2, T3, T4, T5, TResult>(GetMethod(func).Access);
    }

    /// <summary>
    /// Returns a method config for a Func&lt;TResult&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<TResult> LookupFunc<TResult>(object proxy, string methodName, params Type[] types)
    {
      return new FuncConfig<TResult>(LookupMethod<Func<TResult>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, TResult> LookupFunc<T1, TResult>(object proxy, string methodName, params Type[] types)
    {
      return new FuncConfig<T1, TResult>(LookupMethod<Func<T1, TResult>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, TResult> LookupFunc<T1, T2, TResult>(object proxy, string methodName, params Type[] types)
    {
      return new FuncConfig<T1, T2, TResult>(LookupMethod<Func<T1, T2, TResult>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2, T3&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, T3, TResult> LookupFunc<T1, T2, T3, TResult>(object proxy, string methodName, params Type[] types)
    {
      return new FuncConfig<T1, T2, T3, TResult>(LookupMethod<Func<T1, T2, T3, TResult>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2, T3, T4&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, T3, T4, TResult> LookupFunc<T1, T2, T3, T4, TResult>(object proxy, string methodName, params Type[] types)
    {
      return new FuncConfig<T1, T2, T3, T4, TResult>(LookupMethod<Func<T1, T2, T3, T4, TResult>>(proxy, methodName, types).Access);
    }
    /// <summary>
    /// Returns a method config for a Func&lt;TResult, T1, T2, T3, T4, T5&gt; named <paramref name="methodName"/> if exactly one matches
    /// </summary>
    /// <typeparam name="T1">param 1 type</typeparam>
    /// <typeparam name="T2">param 2 type</typeparam>
    /// <typeparam name="T3">param 3 type</typeparam>
    /// <typeparam name="T4">param 4 type</typeparam>
    /// <typeparam name="T5">param 5 type</typeparam>
    /// <typeparam name="TResult">return type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="methodName">method name</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<T1, T2, T3, T4, T5, TResult> LookupFunc<T1, T2, T3, T4, T5, TResult>(object proxy, string methodName, params Type[] types)
    {
      return new FuncConfig<T1, T2, T3, T4, T5, TResult>(LookupMethod<Func<T1, T2, T3, T4, T5, TResult>>(proxy, methodName, types).Access);
    }

    /// <summary>
    /// Returns a method config for a property getter
    /// </summary>
    /// <typeparam name="TResult">property type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="getProp">a lambda expression invoking the proxy's property getter. e.g. () =&gt; proxy.Prop</param>
    /// <returns>method configuration</returns>
    public static FuncConfig<TResult> GetProp<TResult>(object proxy, Func<TResult> getProp)
    {
      //ignore NotImplemented of get_*
      var iprox = GetIProxy(proxy);

      MethodConfig pm = null;
      iprox.ExpectingPropertyUse = true;
      try
      {
        getProp();
        throw new NotImplementedException("GetProp failed. Perhaps the given property was not virtual");
      }
      catch (ExpectingPropertyException epe)
      {
        pm = epe.MethodConfig;
      }

      return new FuncConfig<TResult>(pm);
    }

    /// <summary>
    /// Returns a method config for a property setter
    /// </summary>
    /// <typeparam name="TResult">property type</typeparam>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <param name="setProp">a lambda expression invoking the proxy's property setter. e.g. v =&gt; proxy.Prop = v</param>
    /// <returns>method configuration</returns>
    public static ActionConfig<T1> GetProp<T1>(object proxy, Action<T1> setProp)
    {
      var iprox = GetIProxy(proxy);
      var val = default(T1);

      MethodConfig pm = null;
      iprox.ExpectingPropertyUse = true;

      try
      {
        setProp(val);
        iprox.ExpectingPropertyUse = false;
        throw new NotImplementedException("GetProp failed. Perhaps the given property was not virtual");
      }
      catch (ExpectingPropertyException epe)
      {
        pm = epe.MethodConfig;
      }

      return new ActionConfig<T1>(pm);
    }

    /// <summary>
    /// Returns an IProxy handle of a proxy built by ProxyManager
    /// </summary>
    /// <param name="proxy">proxy built by ProxyManager</param>
    /// <returns>IProxy handle</returns>
    public static IProxy GetIProxy(object proxy)
    {
      var iprox = proxy as IProxy;

      if (iprox == null)
      {
        throw new InvalidOperationException("The given proxy object wasn't actually a proxy");
      }

      return iprox;
    }

    private static void ErrorMethodNotFound(IProxy iprox, MethodInfo methodInfo, string name)
    {
      var types = methodInfo.GetGenericArguments();

      var signature = types.Length == 0 ? methodInfo : methodInfo.GetGenericMethodDefinition();

      var mdSig = new MethodDefinition(signature);

      string msg = null;
      bool nameFound = false;

      foreach (var mi in iprox.GetType().GetMethods())
      {
        var md = new MethodDefinition(mi);

        if (md.Name == name)
          nameFound = true;

        if (md.Matches(mdSig))
        {
          msg =
            string.Format(
            "A method named `{0}' was found but is not virtual. Proxy methods can only be made for virtual methods.",
            name);
          break;
        }
      }

      if (msg == null)
      {
        if (nameFound)
          msg =
            string.Format(
            "A method named `{0}' was found, but did not match the signature you provided: {1}",
            name, mdSig.ToString());
        else
          msg =
            string.Format(
            "A method named `{0}' does not exist in {1}",
            name, iprox.GetType().BaseType.ToString());
      }

      throw new KeyNotFoundException(msg);
    }
  }
}
